Skip to content

HTTP/1.1 vs HTTP/2 深入理解(队头阻塞 & 多路复用)

一、HTTP/1.1 的问题

1. 并发连接限制

浏览器对同一域名的 TCP 连接数量有限制(通常为 6 个):

  • 假设有 20 个请求
  • 浏览器最多建立 6 个 TCP 连接
  • 这 20 个请求会被分配到这 6 个连接中
┌─────────────────────────────────────────────────┐
│              浏览器 (Browser)                    │
│                                                 │
│   请求队列:20 个请求待发送                      │
│   ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐        │
│   │R1│R2│R3│R4│R5│R6│R7│...     │R20│        │
│   └┬─┴┬─┴┬─┴┬─┴┬─┴┬─┴┬─┴──┴──┴──┴┬─┘        │
│    │  │  │  │  │  │  │            │           │
│    ▼  ▼  ▼  ▼  ▼  ▼  ▼            ▼           │
│  ┌────┬────┬────┬────┬────┬────┐             │
│  │TCP1│TCP2│TCP3│TCP4│TCP5│TCP6│ ← 最多 6 个连接│
│  ├────┼────┼────┼────┼────┼────┤             │
│  │R1  │R2  │R3  │R4  │R5  │R6  │             │
│  │R7  │R8  │R9  │R10 │R11 │R12 │             │
│  │R13 │R14 │R15 │R16 │R17 │R18 │             │
│  │R19 │R20 │    │    │    │    │             │
│  └────┴────┴────┴────┴────┴────┘             │
│         │                  │                  │
│         ═════════════════════                  │
│              TCP/IP 网络                        │
│         ═════════════════════                  │
│         │                  │                  │
│         ▼                  ▼                  │
│  ┌────────────┐    ┌────────────┐             │
│  │  服务器     │    │  服务器     │             │
│  │  连接 1     │    │  连接 6     │             │
│  └────────────┘    └────────────┘             │
└─────────────────────────────────────────────────┘

说明:20 个请求被分配到 6 个 TCP 连接,每个连接内串行处理

2. 串行请求问题

在 HTTP/1.1 中:

  • 一个 TCP 连接同一时间只能处理一个请求
  • 必须等待当前请求响应完成,才能发送下一个请求

👉 本质:串行执行

单个 TCP 连接中的时序图:

时间 →  ───────────────────────────────────────────►

客户端                          服务器
   │                              │
   │── [请求 1] ─────────────────>│  100ms
   │                              │
   │<──────── [响应 1] ──────────│  500ms
   │                              │
   │── [请求 2] ─────────────────>│  100ms
   │                              │
   │<──────── [响应 2] ──────────│  500ms
   │                              │
   │── [请求 3] ─────────────────>│  100ms
   │                              │
   │<──────── [响应 3] ──────────│  500ms
   │                              │

总耗时 = (100 + 500) × 3 = 1800ms ⏳

❌ 问题:即使带宽充足,也必须排队等待!

3. 队头阻塞(Head-of-Line Blocking)

由于响应必须按顺序返回:

  • 如果前面的请求慢
  • 后面的请求必须等待

👉 导致:

  • 整个连接被阻塞
  • 性能下降
场景:请求 2 响应很慢(3000ms),阻塞后续所有请求

时间 →  ───────────────────────────────────────────────────────►

客户端                          服务器
   │                              │
   │── [请求 1] ─────────────────>│
   │<──────── [响应 1] ✓─────────│  100ms  ✓ 完成
   │                              │
   │── [请求 2] ─────────────────>│
   │                              │  ⏳ 处理慢...
   │                              │  ⏳ 3000ms...
   │<──────── [响应 2] ──────────│  终于返回
   │                              │
   │── [请求 3] ✗ 等待中 ────────>│  无法发送
   │── [请求 4] ✗ 等待中 ────────>│  无法发送
   │── [请求 5] ✗ 等待中 ────────>│  无法发送
   │                              │

影响:
  ❌ 请求 3、4、5 已准备好,但必须等待请求 2 完成
  ❌ 关键 CSS/JS 文件被阻塞 → 页面白屏
  ❌ 用户体验严重下降

4. Pipeline(管线化)为什么不用?

HTTP/1.1 理论支持 Pipeline:

  • 可以连续发送多个请求

但实际浏览器默认关闭,原因:

  • 响应必须按顺序返回
  • 一个慢请求会阻塞后面所有请求

👉 仍然存在队头阻塞问题

HTTP/1.1 Pipeline 的尝试与失败:

改进点:可以连续发送多个请求 ✓
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
客户端                          服务器
   │                              │
   │── [请求 1][请求 2][请求 3] ─>│  ✓ 一次性发出
   │                              │
   │<── [响应 1] ────────────────│  ✓ 正常返回
   │                              │
   │<── [响应 2 慢⏳] ────────────│  ⏳ 处理中...
   │                              │
   │<── [响应 3] ✗ 被阻塞 ───────│  ✗ 无法返回
   │                              │

❌ 根本问题未解决:响应必须按序返回

二、HTTP/2 的优化

1. 单一 TCP 连接

HTTP/2 的特点:

  • 一个域名只需要 一个 TCP 连接
  • 所有请求都在这个连接中完成
对比图:HTTP/1.1 vs HTTP/2

┌─────────────────────┐      ┌─────────────────────┐
│    HTTP/1.1         │      │     HTTP/2          │
│                     │      │                     │
│  ┌─┐ ┌─┐ ┌─┐ ┌─┐   │      │  ┌───────────────┐  │
│  │R1│ │R2│ │R3│ │R4│ │      │  │ 所有请求      │  │
│  └┬┘ └┬┘ └┬┘ └┬┘   │      │  │ R1 R2 R3 R4  │  │
│   │   │   │   │     │      │  └───────┬───────┘  │
│  ═╝   ═╝   ═╝   ═╝   │      │          ║          │
│  TCP1 TCP2 TCP3 TCP4 │      │      单个 TCP 连接   │
│   │   │   │   │     │      │          ║          │
│  ┌┴─┐ ┌┴─┐ ┌┴─┐ ┌┴┐ │      │  ┌───────┴───────┐  │
│  │S1│ │S2│ │S3│ │S4│ │      │  │   服务器       │  │
│  └──┘ └──┘ └──┘ └─┘ │      │  └───────────────┘  │
└─────────────────────┘      └─────────────────────┘

资源消耗对比:
  HTTP/1.1: 6 次握手 + 6 倍慢启动 + 6 份拥塞控制 = 浪费 🚫
  HTTP/2:   1 次握手 + 1 倍慢启动 + 1 份拥塞控制 = 高效 ✓

2. 多路复用(Multiplexing)

HTTP/2 核心能力:

👉 多个请求可以同时在一个 TCP 连接中并发传输


3. Stream(流)

  • 每个请求对应一个 Stream
  • 20 个请求 = 20 个 Stream
Stream 逻辑通道示意图:

                  TCP 连接 (单条物理通道)
        ════════════════════════════════════════

        逻辑上的独立 Stream(虚拟通道):
        
        Stream 1    Stream 2    Stream 3   ...  Stream 20
           ║           ║           ║              ║
        ┌──╨──┐    ┌──╨──┐    ┌──╨──┐        ┌──╨──┐
        │ HTML │    │ CSS │    │  JS │   ...  │ IMG │
        └──┬──┘    └──┬──┘    └──┬──┘        └──┬──┘
           │           │           │              │
           ║           ║           ║              ║
        ════════════════════════════════════════════
                  共享同一条 TCP 连接
        
✓ 每条 Stream 独立传输,互不干扰
✓ 无优先级依赖,可并行处理

4. Frame(帧)拆分

HTTP/2 会将数据拆分为二进制帧进行传输:

  • 每个 Frame 包含 Stream ID
  • 不同 Stream 的 Frame 可以交错传输
  • 充分利用带宽
数据拆分与交错传输过程:

原始请求:
┌──────────────────────────┐
│  Stream 1: HTML (100KB)  │
│  Stream 2: CSS  (50KB)   │
│  Stream 3: JS   (80KB)   │
└──────────────────────────┘
            ↓ 拆分为二进制帧

帧序列:
Stream 1: [F1-1][F1-2][F1-3][F1-4]...[F1-N]
Stream 2: [F2-1][F2-2][F2-3]
Stream 3: [F3-1][F3-2][F3-3][F3-4]...[F3-M]
            ↓ 交错传输

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
时间片 1: [F1-1][F2-1][F3-1]
时间片 2: [F1-2][F2-2][F3-2]
时间片 3: [F1-3][F2-3][F3-3]
时间片 4: [F1-4][F3-4]
...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

✓ 数据交错发送,不再是排队执行
✓ 充分利用每一刻带宽

👉 特点:

  • 🔀 数据是交错发送的(Interleaved)
  • ⚡ 不再是排队执行
  • 📈 充分利用带宽

5. 数据重组

每个 Frame 都带有:

  • Stream ID

接收端会:

  • 按 Stream ID 重新拼装数据
  • 还原为完整响应
接收端重组流程:

接收到的乱序帧流:
╔════════════════════════════════════════════╗
║ 网络 → [F3-1][F1-1][F2-1][F1-2][F3-2][F2-2] ║
╚════════════════════════════════════════════╝

        根据 Frame Header 中的 Stream ID 分类

    ┌───────────────┬───────────────┬───────────────┐
    │   Stream 1    │   Stream 2    │   Stream 3    │
    ├───────────────┼───────────────┼───────────────┤
    │ [F1-1]        │ [F2-1]        │ [F3-1]        │
    │ [F1-2]        │ [F2-2]        │ [F3-2]        │
    │ ...           │ ...           │ ...           │
    └───────┬───────┴───────┬───────┴───────┬───────┘
            ↓               ↓               ↓
    ┌───────────┐   ┌───────────┐   ┌───────────┐
    │ HTML 完整 │   │ CSS 完整  │   │ JS 完整   │
    └───────────┘   └───────────┘   └───────────┘

三、HTTP/2 是否完全解决队头阻塞?

❗答案:没有完全解决

HTTP/2:

  • ✅ 解决了 应用层队头阻塞
  • ❌ 仍然存在 TCP 层队头阻塞

6. TCP 层队头阻塞

问题原因:

  • HTTP/2 仍然基于 TCP
  • TCP 是可靠传输协议

如果发生丢包:

  • 必须等待重传
  • 整个 TCP 连接被阻塞

👉 影响:

  • 所有 Stream 都会被卡住
TCP 层丢包重传导致的队头阻塞:

正常情况(无丢包):
发送端  ──→ [包 1][包 2][包 3][包 4][包 5] ──→  接收端
           ✓    ✓    ✓    ✓    ✓

              所有 Stream 正常传输

发生丢包:
发送端  ──→ [包 1][包 2][包 3✗][包 4][包 5] ──→  接收端
           ✓    ✓    │    ⏸    ⏸

              检测到丢包,触发重传

           ┌────────┴────────┐
           │  TCP 重传机制   │
           │  [包 3] 重发    │
           └────────┬────────┘

发送端  ──→ [包 1][包 2][包 3✓][包 4][包 5] ──→  接收端
           ✓    ✓    ✓    ✓    ✓

❌ 关键问题:
   包 4、包 5 虽已到达接收端,但无法上交应用层
   必须等待包 3 重传成功并按序重组
   → 所有 Stream(包括 Stream 1、2、3...)都被阻塞

四、HTTP/1.1 vs HTTP/2 对比总结

特性HTTP/1.1HTTP/2
TCP 连接数多个(通常 6 个)单个
请求方式串行并发
数据格式文本二进制
队头阻塞应用层TCP 层
传输方式一个请求一个响应多路复用(交错传输)
带宽利用率低(排队等待)高(充分利用)
握手开销高(多次握手)低(一次握手)
性能对比示意(加载 20 个资源,每个 100ms 发送 + 400ms 响应):

HTTP/1.1(6 个连接,每个连接串行):
连接 1: [R1────→][R7────→][R13───→][R19───→]  = 2000ms
连接 2: [R2────→][R8────→][R14───→][R20───→]  = 2000ms
连接 3: [R3────→][R9────→][R15───→]           = 1500ms
连接 4: [R4────→][R10───→][R16───→]           = 1500ms
连接 5: [R5────→][R11───→][R17───→]           = 1500ms
连接 6: [R6────→][R12───→][R18───→]           = 1500ms
整体完成时间:2000ms(取决于最慢的连接)

HTTP/2(单连接,多路复用):
单连接:[R1↗R2↗R3↗R4↗R5↗...↗R20]  = 600ms ✓
        (所有请求几乎同时完成)

📊 性能提升:2000ms / 600ms ≈ 3.3 倍!

五、面试标准回答

HTTP/1.1 存在队头阻塞问题,一个 TCP 连接同一时间只能处理一个请求,浏览器通常会建立最多 6 个 TCP 连接来提升并发,但整体仍然是串行处理。

HTTP/2 引入多路复用机制,在一个 TCP 连接中可以并发多个请求。每个请求会被拆分为多个二进制帧(Frame),在同一个连接中交错传输,接收端再根据 Stream ID 进行重组,从而解决了应用层的队头阻塞问题。

不过 HTTP/2 仍然基于 TCP,如果发生丢包,会导致 TCP 层的队头阻塞,影响所有请求。


六、延伸思考

HTTP/3 如何彻底解决队头阻塞?

协议演进路线图:

HTTP/1.1              HTTP/2                HTTP/3
   │                     │                     │
   │  基于 TCP           │  基于 TCP           │  基于 UDP
   │  ✗ 应用层阻塞       │  ✓ 解决应用层       │  (QUIC 协议)
   │  ✗ 多连接浪费       │  ✗ TCP 层阻塞       │  ✓ 无连接状态
   │                     │                     │  ✓ 0-RTT 握手
   │                     │                     │  ✓ 独立流控
   │                     │                     │  ✓ 无队头阻塞
   ▼                     ▼                     ▼

解决程度:
  HTTP/1.1  ████████░░░░░░░░  40%  (完全未解决)
  HTTP/2    ████████████░░░░  70%  (解决应用层,遗留 TCP 层)
  HTTP/3    ████████████████  100% (彻底解决) ✓

HTTP/3 的关键创新 - QUIC 协议:

QUIC 的多路复用机制:

┌──────────────────────────────────────────────┐
│            UDP 数据包                         │
│  ┌────────────┬────────────┬────────────┐   │
│  │ Stream 1   │ Stream 2   │ Stream 3   │   │
│  │   独立帧    │   独立帧    │   独立帧    │   │
│  └────────────┴────────────┴────────────┘   │
│                                              │
│  ✓ 每个 Stream 独立确认和重传                │
│  ✓ 一个 Stream 丢包不影响其他 Stream         │
│  ✓ 真正彻底解决队头阻塞                      │
└──────────────────────────────────────────────┘

七、记忆口诀

HTTP 协议演进歌诀:

HTTP/1.1 问题多,
六个连接排排坐。
队头阻塞跑不掉,
Pipeline 也没用着。

HTTP/2 来改进,
单连接里多路行。
帧交错传效率高,
TCP 阻塞仍头疼。

HTTP/3 用 QUIC,
UDP 上建奇功。
彻底解决阻塞患,
零 RTT 握手如风!

八、总结一句话

  • HTTP/1.1:多连接 + 串行 + 应用层阻塞 🚫
  • HTTP/2:单连接 + 多路复用 + TCP 层阻塞 ⚠️
  • HTTP/3:单连接 + QUIC 协议 + 彻底解决阻塞 ✓
最近更新